// Copyright (c) 2010 SuccessFactors, Inc.
// All rights reserved.
//
// Redistribution and use in source and binary forms, with or without
// modification, are permitted provided that the following conditions
// are met:
//
//     * Redistributions of source code must retain the above
//       copyright notice, this list of conditions and the following
//       disclaimer.
//
//     * Redistributions in binary form must reproduce the above
//       copyright notice, this list of conditions and the following
//       disclaimer in the documentation and/or other materials
//       provided with the distribution.
//
//     * Neither the name of the SuccessFactors, Inc. nor the names of
//       its contributors may be used to endorse or promote products
//       derived from this software without specific prior written
//       permission.
//
// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
// FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
// COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
// INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES
// (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
// SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
// HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
// STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
// ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
// OF THE POSSIBILITY OF SUCH DAMAGE.

package org.owasp.jxt.servlet;

import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.Set;
import javax.servlet.ServletConfig;
import javax.servlet.ServletContext;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.owasp.jxt.CompileException;
import org.owasp.jxt.JxtCompilation;
import org.owasp.jxt.JxtConfigException;
import org.owasp.jxt.JxtEngine;
import org.owasp.jxt.LocatedMessage;
import org.owasp.jxt.QuickCache;
import org.owasp.jxt.compiler.Compiler;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

/**
 * JxtServlet
 *
 * @author Jeffrey Ichnowski
 * @version $Revision: 13 $
 */
public class JxtServlet extends HttpServlet {

    private static final Logger _log = LoggerFactory.getLogger(JxtServlet.class);

    /** From Servlet Spec, attribute that has the name of the
     * web-app's temporary directory */
    private static final String TEMPDIR_ATTRIBUTE = "javax.servlet.context.tempdir";

    /** The classpath the Tomcat/Catalina uses for JSP compilation.
     * This is unfortunately specific to one container.  There does
     * not appear to be a standard way to retrieve it across all
     * servlet containers. */
    private static final String CATALINA_JSP_CLASSPATH =
        "org.apache.catalina.jsp_classpath";

    /** From Tomcat: "If development is false and checkInterval is
     * greater than zero, background compilations are
     * enabled. checkInterval is the time in seconds between checks to
     * see if a JSP page needs to be recompiled. [0]" */
    private static final String INITPARAM_CHECK_INTERVAL =
        "checkInterval";

    /** From Tomcat: "Causes a JSP (and its dependent files) to not be
     * checked for modification during the specified time interval (in
     * seconds) from the last time the JSP was checked for
     * modification. A value of 0 will cause the JSP to be checked on
     * every access.  Used in development mode only. [4]" */
    private static final String INITPARAM_MODIFICATION_TEST_INTERVAL =
        "modificationTestInterval";

    /** The default modification test interval. */
    private static final int DEFAULT_MODIFICATION_TEST_INTERVAL = 4;

    /** From Tomcat: Which compiler Ant should use to compile JSP
     * pages.  See the Ant documentation for more
     * information. [javac] */
    private static final String INITPARAM_COMPILER =
        "compiler";

    private static final String INITPARAM_COMPILER_SOURCE_VM =
        "compilerSourceVM";

    private static final String INITPARAM_COMPILER_TARGET_VM =
        "compilerTargetVM";

    private static final String INITPARAM_CLASSDEBUGINFO =
        "classdebuginfo";

    private static final String INITPARAM_CLASSPATH =
        "classpath";

    /** From Tomcat: "Is Jasper used in development mode? If true, the
     * frequency at which JSPs are checked for modification may be
     * specified via the modificationTestInterval
     * parameter. [true]" */
    private static final String INITPARAM_DEVELOPMENT =
        "development";

    private static final String INITPARAM_ENABLE_POOLING =
        "enablePooling";

    private static final String INITPARAM_FORK =
        "fork";

    private static final String INITPARAM_IE_CLASS_ID =
        "ieClassId";

    private static final String INITPARAM_JAVA_ENCODING =
        "javaEncoding";

    private static final String INITPARAM_KEEPGENERATED =
        "keepgenerated";

    private static final String INITPARAM_MAPPEDFILE =
        "mappedfile";

    private static final String INITPARAM_TRIMSPACES =
        "trimSpaces";

    private static final String INITPARAM_SUPPRESS_SMAP =
        "suppressSmap";

    private static final String INITPARAM_DUMP_SMAP =
        "dumpSmap";

    private static final String INITPARAM_GEN_STR_AS_CHAR_ARRAY =
        "genStrAsCharArray";

    private static final String INITPARAM_ERROR_ON_USE_BEAN_INVALID_CLASS_ATTRIBUTE =
        "errorOnUseBeanInvalidClassAttribute";

    private static final String INITPARAM_SCRATCHDIR =
        "scratchdir";

    private static final String INITPARAM_XPOWERED_BY =
        "xpoweredBy";

    /** Milliseconds per second. */
    private static final long MS_PER_SECOND = 1000L;

    private JxtEngine _engine;
    private WrapperCache _jxtWrapperCache;

    private static boolean booleanInitParam(ServletConfig config, String param, boolean defValue) {
        String value = config.getInitParameter(param);
        if (null == value) {
            return defValue;
        }
        if ("true".equalsIgnoreCase(value) ||
            "yes".equalsIgnoreCase(value) ||
            "1".equals(value))
        {
            return true;
        }
        if ("false".equalsIgnoreCase(value) ||
            "no".equalsIgnoreCase(value) ||
            "0".equalsIgnoreCase(value))
        {
            return false;
        }

        _log.warn("Invalid value for "+param+" parameter, expected boolean, got: "+value);

        return defValue;
    }

    private static int integerInitParam(ServletConfig config, String param, int defValue) {
        String value = config.getInitParameter(param);
        if (null == value) {
            return defValue;
        }
        try {
            return Integer.parseInt(value);
        } catch (NumberFormatException e) {
            _log.warn("Invalid value for "+param+" parameter, expected an integer, got: "+value);
            return defValue;
        }
    }

    /**
     * Initializes the Servlet Wrapper Cache based upon the servlet
     * initialization parameters.
     */
    private WrapperCache initWrapperCache(ServletConfig config) {
        long intervalBetweenStaleChecks;

        boolean developmentMode = booleanInitParam(config, INITPARAM_DEVELOPMENT, true);
        if (!developmentMode) {
            intervalBetweenStaleChecks = QuickCache.NO_STALE_CHECKS;

            int checkInterval = integerInitParam(config, INITPARAM_CHECK_INTERVAL, 0);

            if (checkInterval > 0) {
                // TODO: support.
                _log.warn(INITPARAM_CHECK_INTERVAL+" is not supported");
            }

        } else {
            int modTestInterval = integerInitParam(
                config, INITPARAM_MODIFICATION_TEST_INTERVAL,
                DEFAULT_MODIFICATION_TEST_INTERVAL);

            if (modTestInterval < 0) {
                modTestInterval = DEFAULT_MODIFICATION_TEST_INTERVAL;
                _log.warn("Invalid "+INITPARAM_MODIFICATION_TEST_INTERVAL+" value, got "+
                          modTestInterval+", but must be >= 0");
            }

            intervalBetweenStaleChecks = modTestInterval * MS_PER_SECOND;
        }

        return new WrapperCache(intervalBetweenStaleChecks);
    }

    private Compiler createCompiler(ServletConfig config) {
        String name = config.getInitParameter(INITPARAM_COMPILER);

        if (null == name) {
            return Compiler.newInstance();
        }

        try {
            return Compiler.forName(name);
        } catch (IllegalArgumentException e) {
            _log.warn("Could not create compiler", e);
            return Compiler.newInstance();
        }
    }

    private Compiler initCompiler(ServletConfig config) {
        Compiler compiler = createCompiler(config);

        compiler.setSource(config.getInitParameter(INITPARAM_COMPILER_SOURCE_VM));
        compiler.setTarget(config.getInitParameter(INITPARAM_COMPILER_TARGET_VM));
        compiler.setDebug(booleanInitParam(config, INITPARAM_CLASSDEBUGINFO, true));

        return compiler;
    }

    @Override
    public final void init(ServletConfig config) throws ServletException {
        super.init(config);

        ServletContext ctx = config.getServletContext();

        // Useful code to find out available servlet context attributes.
//         _log.info("ServletContext attributes follow");
//         for (java.util.Enumeration e = ctx.getAttributeNames() ; e.hasMoreElements() ;) {
//             String name = (String)e.nextElement();
//             _log.info("  {} = {}", name, ctx.getAttribute(name));
//         }

        // Apache Tomcat puts their JSP engine's classpath in this
        // attribute.
        String classPath = (String)ctx.getAttribute(CATALINA_JSP_CLASSPATH);

        File tempDir = (File)ctx.getAttribute(TEMPDIR_ATTRIBUTE);

        Compiler compiler = initCompiler(config);

        try {
            _engine = JxtEngine.builder()
                .classPath(classPath)
                .webRoot(new File(ctx.getRealPath("/")))
                .tempDir(tempDir)
                .build();

        } catch (JxtConfigException e) {
            throw new ServletException("Configuration Error", e);
        }

        _jxtWrapperCache = initWrapperCache(config);

        _log.info("Initialized (version {})", _engine.getVersion());
        _log.debug("Class-Path = {}", classPath);
    }

    @Override
    public final void destroy() {
        _engine = null;
        _log.info("Destroyed");
    }

    @Override
    public final void service(HttpServletRequest request,
                              HttpServletResponse response)
        throws ServletException, IOException
    {
        // The "javax.servlet.include" attributes are for handling
        // RequestDispatcher.included .jxt files.  The original
        // request will have all parameters coming from the standard
        // servlet request APIs.  I'm not sure if this is a standard,
        // but it's what tomcat supports and they are "javax."
        // attributes (not "org.apache")

        String jxtFile = (String)request.getAttribute("javax.servlet.include.servlet_path");
        String pathInfo;

        if (jxtFile != null) {
            pathInfo = (String)request.getAttribute("javax.servlet.include.path_info");
        } else {
            jxtFile = request.getServletPath();
            pathInfo = request.getPathInfo();
        }

        if (pathInfo != null) {
            jxtFile += pathInfo;
        }

        // TODO: protect WEB-INF

        try {
            Wrapper wrapper = _jxtWrapperCache.get(jxtFile);
            _log.debug("Including {}", jxtFile);
            wrapper.service(request, response);
        } catch (JxtFileNotFoundException e) {
            _log.warn("Could not find {}", jxtFile);
            response.sendError(
                HttpServletResponse.SC_NOT_FOUND,
                request.getRequestURI());
        }
    }

    /**
     * Wraps compiled instances of JXT files.  This handles tracking
     * the source files used to create ths instance so that stale
     * checks may be performed.
     */
    class Wrapper {
        private String _name;
        private JxtServletBase _instance;
        private ServletException _initException;
        private List<File> _sourceFiles;
        private long _initTime;

        public Wrapper(String name) {
            _name = name;

            try {
                _log.debug("init {}", _name);

                _initTime = System.currentTimeMillis();

                JxtCompilation<JxtServletBase> compilation =
                    _engine.compileServlet(_name);

                for (LocatedMessage warning : compilation.getWarnings()) {
                    _log.warn(warning.toString());
                }

                _instance = compilation.getTemplateClass().newInstance();
                _instance.init(getServletConfig());

                setSourceFiles(compilation.getSourceFiles());
            } catch (ServletException e) {
                _initException = e;
            } catch (CompileException e) {
                _initException = new ServletException("JXT Compilation Failed", e);
                setSourceFiles(e.getSourceFiles());
            } catch (InstantiationException e) {
                _initException = new ServletException("JXT Instantiation Exception", e);
            } catch (IllegalAccessException e) {
                _initException = new ServletException("JXT Access Exception", e);
            } catch (RuntimeException e) {
                _initException = new ServletException("JXT Initialization Exception", e);
            } catch (Error e) {
                _initException = new ServletException("JXT Initialization Error", e);
            }
        }

        private void setSourceFiles(Set<String> sourceFiles) {
            _sourceFiles = new ArrayList<File>(sourceFiles.size());

            ServletContext ctx = getServletContext();

            for (String sourceName : sourceFiles) {
                String realPath = ctx.getRealPath(sourceName);
                if (realPath == null) {
                    realPath = sourceName;
                }
                _sourceFiles.add(new File(realPath));
            }
        }

        @SuppressWarnings("deprecation")
        private boolean isSingleThreadModel() {
            return _instance instanceof javax.servlet.SingleThreadModel;
        }

        /**
         * Performs the stale check on a single JXT (and all its
         * dependencies).  Note: the lastmodificationtime check comes
         * from the calling WrapperCache/QuickCache.
         */
        public boolean isStale() {
            if (_sourceFiles == null) {
                // init failed, check the main file for an update
                File file = new File(getServletContext().getRealPath(_name));
                return file.exists() && file.lastModified() > _initTime;
            } else {
                for (File file : _sourceFiles) {
                    if (file.exists() && file.lastModified() > _initTime) {
                        return true;
                    }
                }
                return false;
            }
        }

        public void service(HttpServletRequest request,
                            HttpServletResponse response)
            throws ServletException, IOException
        {
            if (_initException != null) {
                throw _initException;
            }

            if (isSingleThreadModel()) {
                synchronized (this) {
                    _instance.service(request, response);
                }
            } else {
                _instance.service(request, response);
            }
        }

        public void destroy() {
            if (_instance != null) {
                _instance.destroy();
            }
        }
    }

    /**
     * Cache for wrappers.
     */
    class WrapperCache extends QuickCache<String,Wrapper> {
        WrapperCache(long intervalBetweenStaleChecks) {
            super(intervalBetweenStaleChecks);
        }

        @Override
        protected Wrapper resolve(String jxtFile) {
            File file = new File(getServletContext().getRealPath(jxtFile));

            // Check that the file exists first to prevent a potential
            // DOS attack based upon filling up the _jxtWrapperCache.
            // We'll only have one Wrapper entry per file in the war.

            if (!file.exists()) {
                throw new JxtFileNotFoundException();
            }

            return new Wrapper(jxtFile);
        }

        @Override
        protected boolean isStale(String jxtFile, Wrapper wrapper) {
            return wrapper.isStale();
        }
    }

    private static class JxtFileNotFoundException extends RuntimeException {
    }

} // JxtServlet
